Lab10: 데이터로 소통하기: 동적, 반응형 시각화

Author

이상일(서울대학교 지리교육과)
김세창(서울대학교 지리교육과 석사)
김우형(서울대학교 지리교육과 석사과정)

Modified

May 21, 2024

실습 개요

이번 실습은 대시보드의 구성 요소로 활용할 수 있는 다양한 시각화 기법을 익히는 것이다.

소통을 위한 시각화 재료로서 정적인 도표가 가지는 절대적인 중요성에도 불구하고, 상호작용성(interactivity)과 역동성(animatedness)이 부가된 도표는 어떤 상항에서 소통의 본질적 가치를 고양하는데 많은 도움을 줄 수 있다.

동시에 텍스트, 테이블, 차트와 같은 것들 보다 훨씬 더 효과적으로 정보를 전달할 수 있는 지도(maps) 역시 만들어볼 것이다.

tidyverse 패키지는 기본이고, gapminder 데이터를 실습의 여러 곳에서 활용할 것이다.

library(tidyverse)
library(gapminder)

1 테이블(Tables)

우리는 지금까지 테이블의 중요성에 대해 거의 다루지 않았다. 그러나 상호작용형 테이블 혹은 대화형 테이블이 되었을 때, 많은 경우, 테이블은 가장 효과적인 정보 전달 도구가 된다. 특히, 데이터 변형하기를 통해 새로운 요약 테이블을 생성하고, 그것을 대화형으로 제시하는 것은 매우 중요한 데이터사이언스의 과정이다. 여기서는 DT(Data Tables) 패키지를 활용하여 간단한 대화형 테이블을 만들어 본다.

library(DT)

그리고 gapminder 데이터를 datatable() 함수를 통해 불러온다.

datatable(gapminder)


DT 패키지는 테이블의 상호작용성과 관련하여 몇 가지 기능을 제공한다.

  • Instant search: 즉각적인 찾기 기능(Search에 타이핑하기 시작하면 즉각적으로 검색 결과 보여줌)

  • Multi-column ordering: 다중 컬럼 정렬 기능(컬럼 하나를 선택한 후 ctrl을 누른 상태에서 다른 컬럼을 선택)

  • filtering: 값을 정렬할 수 있는 기능

  • editable: 셀 값을 수정할 수 있는 기능

  • Pagination: 페이지를 이동할 수 있는 기능

  • Buttons: 셀 숨기기, CSV, PDF, XLSX 등의 확장자로 내보내기 등을 수행하는 버튼 생성 기능

그 중 몇 가지 기능을 여기에서 살펴본다. 다음의 웹사이트를 참고할 수 있다.

https://rstudio.github.io/DT/

1.1 테이블 에디팅

editable 아규먼트를 통해 테이블의 값을 수정할 수 있게 만들 수 있다. 테이블의 특정 셀에 더블클릭하면 수정할 수 있다.

datatable(head(gapminder), editable = "cell")

1.2 컬럼 필터

다음과 같은 방식으로 필터를 설정할 수 있다. 동시에 한 페이지에 보이는 행의 개수, 너비 자동 지정 등도 함께 추가한다.

datatable(gapminder, filter = "top", 
          options = list(
            pageLength = 5, autoWidth = TRUE
          ))

1.3 버튼 기능

extenstion에 Buttons, dom 에 Bftip, buttonsc("copy", "excel", "pdf", "print") 를 입력하면 버튼 기능을 활성화할 수 있다. 각 아규먼트가 무엇을 의미하는지는 다음의 사이트를 참고할 것. https://datatables.net/extensions/buttons/

datatable(gapminder, filter = "top",
          extensions = "Buttons",
          options = list(
            pageLength = 5,
            autoWidth = TRUE,
            dom = "Bftip",
            buttons=c("copy", "excel", "pdf", "print")
          ))

이 밖에도 아래와 같은 다양한 기능을 제공한다. 이를 참고하면 테이블의 모양을 조금 더 자유롭게 수정할 수 있다.

1.4 테이블 CSS 클래스

datatable 함수의 class 아규먼트: 테이블의 외관 수정

Class name Description
display stripe, hover, row-border, order-column을 동시 적용한 디폴트
cell-border 모든 셀의 상하좌우에 경계선 표시
compact 여백 축소
hover 마우스의 위치에 따라 점멸 효과
nowrap 줄바꿈 없이 텍스트 표시
order-column 정렬의 키가 되는 컬럼에 하이라이트 표시
row-border 행별 경계선 표시
stripe 행을 스트라이프로 표시

아래와 같이 cell-border과 compact를 함께 실행해 보고 테이블의 변화를 확인한다.

datatable(head(gapminder), class = "compact stripe")


또한 특정 컬럼의 정렬 방식(왼편, 오른편, 중앙)을 변경할 수 있다. 사용가능한 옵션은 다음과 같다.

Class name Description
dt[-head|-body]-left 왼편 정렬
dt[-head|-body]-center 가운데 정렬
dt[-head|-body]-right 오른편 정렬
dt[-head|-body]-justify 양쪽 맞춤
dt[-head|-body]-nowrap 줄바꿈 없는 맞춤

아래는 첫 번째와 두 번째 컬럼(country, continent)의 내용(body)을 가운데 정렬로 나타낸다.

datatable(head(gapminder),
          options = list(
            columnDefs = list(list(className = "dt-body-center", targets = 1:2)) 
            ))

2 그래프(Graphs)

2.1 반응형 그래프

Plotly는 보통 데이터 시각화용 JavaScript 라이브러리를 일컽는다. 이 라이브러리는 다양한 오픈소스 프로그래밍 언어에서 사용가능하며, R의 랩퍼 프로그램이 plotly 패키지이다

library(plotly)

먼저 gapminder 데이터를 이용하여 간단한 그래프를 그려보자. 문법이 ggplot2와 크게 다르지 않음을 알 수 있다.

gapminder |> 
  filter(year == 2007) |> 
  plot_ly(x = ~gdpPercap, y = ~lifeExp, color = ~continent,
          text = ~paste("Country: ", country, 
                        "<br>GDP per capita: ", gdpPercap, 
                        "$<br>Life Expectancy at Birth:", lifeExp))

다음 예제 역시 반응형이지만 바로 다음에서 다룰 동적인 특성도 동시에 가지고 있는 그래프를 만드는 것이다. 역시 plotly 패키지를 이용한다.

gapminder |> 
  plot_ly(x = ~log10(gdpPercap), y = ~lifeExp,
          text = ~paste("Country:", country, "</br>Continent:", continent, 
"</br>lifeExp:", lifeExp)) |> 
  add_markers(color = ~continent, size = ~pop, frame = ~year, 
              marker = list(sizeref = 0.2, sizemode = "area"))

그러나 지금까지 실습 수업에서는 ggplot()을 사용해서 그래프를 그려왔기 때문에, 아무래도 그쪽이 더 익숙할 것이다. 다행히도 plotly 패키지가 제공하는 ggplotly() 함수를 활용하면 ggplot2로 만들어진 그래프를 단숨에 plotly 그래프로 바꿀 수 있다. 물론 정확히 같지는 않다.

P <- gapminder |> 
  filter(year == 2007) |> 
  ggplot(aes(x = gdpPercap, y = lifeExp, color = continent)) +
  geom_point() + 
  scale_color_brewer(palette = "Set2") +
  theme_minimal()

ggplotly(P)

2.2 동적 그래프

이번에는 동적 그래프(animated graphs)를 만들어본다. gganimate 패키지(https://gganimate.com/)를 활용하기 위해, 우선 gganimate 패키지를 설치하고 불러온다.

library(gganimate)

동적인 그래프의 효과를 알기 위해, 먼저 정적인 지도를 그려보자.

P <- gapminder |> 
  ggplot(aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
  geom_point(show.legend = FALSE, alpha = 0.7) +
  scale_x_log10() +
  scale_size(range = c(2, 12))
P

이 그래프는 두 변수 간에 양적인 관련성이 있다는 사실은 명백히 보여주지만, 데이터 변형의 측면에서는 잘못된 것이다. 모든 연도(1952~2007년간 5년 단위)가 나타나 있어서 한 국가가 그래프에 12번 등장한다.

정적인 그래프에서 이를 극복하기 위해서는 ggplot2 패키지의 facet_wrap() 함수를 활용할 수 있다.

P + facet_wrap(~year)

이 그래프는 두 변수간의 양적인 상관관계가 12개 모두의 연도에서 나타난다는 사실을 명확히 보여준다. 그러나 그래프를 세세히 살펴보면 알 수 있듯이, 두 변수의 관련성이라는 측면에서 개별 국가가 시간의 흐름에 따라 어떻게 변화해 나가는지에 대한 사항을 파악하기는 매우 어렵다.

gganimate 패키지의 transition_time() 함수를 활용하여 동적인 그래프를 작성해 본다.

P + transition_time(year) +
  labs(title = "Year: {frame_time}")
NULL

대륙별로 분할하여 표현할 수도 있다.

P + facet_wrap(~continent) +
  transition_time(year) +
  labs(title = "Year: {frame_time}")
NULL

그래프를 저장하고 싶으면 anim_save() 함수를 활용할 수 있다. ggsave() 함수와 동일한 문법을 갖는다.

P + facet_wrap(~continent) +
  transition_time(year) +
  labs(title = "Year: {frame_time}")

anim_save("anim_P.gif")

3 지도(Maps)

3.1 정적 지도

3.1.1 세계 지도

ggplot2 패키지를 이용하여 정적 지도를 그려본다. 데이터는 지난 실습에서 사용한 WPP 2022(World Population Prospects 2022)이다. 2024년 전세계 국가별 TFR(Total Fertility Rate, 합계출산율) 지도를 그려본다.

일반적으로 지도는 형상 데이터와 속성 데이터를 결합해야만 제작할 수 있다. 여기서 형상 데이터는 전세계 국가 경계 데이터이고, 속성 데이터는 TFR이 포함된 WPP 2022 데이터이다. 형상 데이터는 spData 패키지에 들어 있는 world를 사용한다. 이러한 형상 데이터를 다루는데 가장 널리 사용되고 있는 중요한 것이 sf 패키지(https://r-spatial.github.io/sf/)이다. 가능한 벡터 형식의 데이터는 sf 패키지의 st_as_sf() 함수를 통해 sf 객체로 변환하는 것이 좋다.

library(spData)
library(sf)
data(world)
world <- st_as_sf(world)

WPP 2022 데이터를 불러와 2024년만 골라낸다.

wpp_2022 <- read_rds("wpp_2022.rds")
my_wpp <- wpp_2022 |> 
  filter(year == 2024)

두 데이터를 left_join 함수를 이용하여 결합한다.

world_data <- world |>
  left_join(my_wpp, join_by(iso_a2 == ISO2))

로빈슨 도법(Robinson projection)의 지도를 제작한다. ggplot2 패키지로 지도를 그리는 가장 좋은 방법은 geom_sf()와 coord_sf()를 결합하는 것이다.

world_map <- ggplot() +
  geom_sf(data = world_data, aes(fill = TFR, text = name_long)) +
  coord_sf(crs = "+proj=robin") +
  scale_fill_viridis_c() +
  scale_x_continuous(breaks = seq(-180, 180, 30)) +
  scale_y_continuous(breaks = c(-89.5, seq(-60, 60, 30), 89.5)) +
  theme(
    panel.background = element_rect("white"),
    panel.grid = element_line(color = "gray80")
  )
world_map

3.1.2 우리나라 지도

먼저 eTL에 탑재되어 있는 파일(sido.shp, sigungu.shp)을 다운로드하고, 아래의 코드를 통해 불러온다.

sido_shp <- st_read("sido.shp", options = "ENCODING=CP949")
options:        ENCODING=CP949 
Reading layer `sido' from data source 
  `D:\R_Websites\Subjects\Data_Science\sido.shp' using driver `ESRI Shapefile'
Simple feature collection with 17 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 746255.1 ymin: 1458982 xmax: 1387941 ymax: 2068161
Projected CRS: Korea_2000_Korea_Unified_Coordinate_System
sigungu_shp <- st_read("sigungu.shp", options = "ENCODING=CP949")
options:        ENCODING=CP949 
Reading layer `sigungu' from data source 
  `D:\R_Websites\Subjects\Data_Science\sigungu.shp' using driver `ESRI Shapefile'
Simple feature collection with 229 features and 9 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 746255.1 ymin: 1458982 xmax: 1387941 ymax: 2068161
Projected CRS: Korea_2000_Korea_Unified_Coordinate_System

또한 지역소멸위험지수 데이터를 불러온다. 아래 코드는 지난번 실습 때 rds 파일 포맷으로 저장해 둔 것을 가정한 것이다. 실습의 편의를 위해 파일을 제공하니 프로젝트 폴더에 저장한 후, 아래의 코드를 통해 불러온다.

data_sigungu <- read_rds("data_sigungu.rds")

다음으로 도형 데이터(sigungu_shp)와 속성 데이터(data_sigungu)를 공통 키(key)를 활용하여 결합한다.

sigungu_data <- sigungu_shp |> 
  left_join(
    data_sigungu, join_by(SGG1_CD == C1)
  )

이제 ggplot2 패키지를 이용하여 지도를 제작한다. 탭셋을 바꿔가며 ’Lab07: 데이터 수집하기’에서 인구소멸위험지수의 시도별 그래프를 제작한 것과 비교해 보면, 매우 유사하다는 것을 알 수 있다. ggplot2에서는 그래프와 지도의 구분이 없다. 이것은 ggplot2의 장점이자 단점이다.

sigungu_data <- sigungu_data |> 
  mutate(
    index_class = case_when(
      index < 0.2 ~ "1",
      index >= 0.2 & index < 0.5 ~ "2",
      index >= 0.5 & index < 1.0 ~ "3",
      index >= 1.0 & index < 1.5 ~ "4",
      index >= 1.5 ~ "5"
    ),
    index_class = factor(index_class, levels = as.character(1:5))
  )

class_color <- c("1" = "#d7191c", "2" = "#fdae61",
                 "3" = "#ffffbf", "4" = "#a6d96a", 
                 "5" = "#1a9641")
ggplot() +
  geom_sf(data = sigungu_data, aes(fill = index_class), show.legend = TRUE) +
  geom_sf(data = sido_shp, fill = NA, lwd = 0.5) +
  scale_fill_manual(name = "Classes", 
                    labels = c("< 0.2", "0.2 ~ 0.5", "0.5 ~ 1.0", 
                               "1.0 ~ 1.5", ">= 1.5"), 
                    values = class_color, drop = FALSE) 

data_sido <- data_sido |> 
  mutate(
    index_class = case_when(
      index < 0.2 ~ "1",
      index >= 0.2 & index < 0.5 ~ "2",
      index >= 0.5 & index < 1.0 ~ "3",
      index >= 1.0 & index < 1.5 ~ "4",
      index >= 1.5 ~ "5"
    ),
    index_class = factor(index_class, levels = as.character(1:5))
  )

class_color <- c("1" = "#d7191c", "2" = "#fdae61",
                 "3" = "#ffffbf", "4" = "#a6d96a", 
                 "5" = "#1a9641")
data_sido |> 
  ggplot(aes(x = index, y = fct_reorder(C1_NM, index))) +
  geom_col(aes(fill = index_class), show.legend = TRUE) +
  geom_text(aes(label = format(round(index, digits = 3), 
                               nsmall = 3)), hjust = -0.1) +
  scale_x_continuous(limits = c(0, 1.5)) +
  scale_fill_manual(name = "Classes", 
                    labels = c("< 0.2", "0.2 ~ 0.5", "0.5 ~ 1.0", 
                               "1.0 ~ 1.5", ">= 1.5"), 
                    values = class_color, drop = FALSE) +
  labs(title = "인구소멸위험지수, 2022년", 
       x = "인구소멸위험지수", 
       y = "")

3.2 반응형 지도

위에서 사용한 plotly 패키지의 ggplotly() 함수를 활용하면 반응형 지도를 생성할 수 있다. 지도의 코드 중 달라진 것은 둘째 줄 aes()에 text = name_long이 첨가되었다는 것이다. 마우스로 국가를 가리킬 때 이름이 나타날 수 있게 조치한 것이다.

ggplotly(world_map)

우리나라 지도는 다른 방식으로 반응형으로 만들어 본다. 여기서는 ggiraph 패키지를 사용한다. 처음 사용하는 패키지이므로 인스톨을 먼저 해야 한다. 코드는 크게 세 부분으로 나뉜다.

  • 전반부: 커서를 특정 시군구 위에 올렸을 때 나타나는 정보를 좀 더 다양하게 하려는 조치이다.

  • 중반부: 실제 지도를 그리는 핵심 코드이다. 산출물에 커서를 올렸을 때 나타나는 정보는 전반부에 작성한 코드가 포함되어 있기 때문에 나타나는 것이다.

  • 후반부: 커서를 올렸을 때 색이 회색으로 변하게 만들어주는 부가 코드

library(ggiraph)
sigungu_data <- sigungu_data |> 
  mutate(
    index = format(index, digits = 4, nsmall = 4),
    my_tooltip = str_c("Name: ", SGG1_FNM, "\n Index: ", index)
  )
gg <- ggplot() +
  geom_sf_interactive(data = sigungu_data, 
                      aes(fill = index_class, tooltip = my_tooltip, data_id = SGG1_FNM), show.legend = TRUE) +
  geom_sf(data = sido_shp, fill = NA, lwd = 0.5) +
  scale_fill_manual(name = "Classes", 
                    labels = c("< 0.2", "0.2 ~ 0.5", "0.5 ~ 1.0", 
                               "1.0 ~ 1.5", ">= 1.5"), 
                    values = class_color, drop = FALSE) 
girafe(ggobj = gg) |> 
  girafe_options(
    opts_hover(css = "fill: gray")
  )

그러나 반응형 지도 제작에 가장 널리 쓰이는 것은 leaflet이다. leaflet은 웹 상의 반응형 지도 제작에 특화된 JavaScript 라이브러리이다(https://leafletjs.com/). 이 라이브러리를 R에서 쓸 수 있게 도와주는 래퍼 패키지가 leaflet 패키지이다(https://rstudio.github.io/leaflet/).

위에서 작성했던 TFR 세계지도를 leaflet 패키지의 다양한 함수와 아규먼트를 활용하여 반응형 지도를 제작해 본다. 코드에 대한 자세한 설명은 생략하니, 구글링과 조교의 도움을 받도록 한다.

library(leaflet)
bins <- c(0, 1.5, 2.1, 3, 4, 5, Inf)
pal <- colorBin("YlOrRd", domain = world_data$TFR, bins = bins)
labels <- sprintf("<strong>%s</strong><br/>%g",
  world_data$name_long, world_data$TFR) |> lapply(htmltools::HTML)

leaflet(world_data) |> 
  addProviderTiles(providers$Esri.WorldTopoMap) |> 
  addPolygons(
    fillColor = ~pal(TFR),
    weight =  0.5, 
    opacity = 1,
    color = "white",
    dashArray = "3",
    fillOpacity = 0.6,
    highlightOptions = highlightOptions(
      weight = 5,
      color = "#666",
      dashArray = "",
      fillOpacity = 0.6,
      bringToFront = TRUE),
    label = labels,
    labelOptions = labelOptions(
      style = list("font-weight" = "normal", padding = "3px 8px"),
      textsize = "15px",
      direction = "auto")
  ) |> 
  addLegend(
    pal = pal, values = ~TFR, opacity = 0.6, title = NULL,
    position = "bottomright"
  )

이 코드를 잘 활용하면 우리나라 시군구 지도를 완전한 반응형으로 만들 수 있다. 한번 시도해 보면 좋을 것 같다.

(참고: https://sechangkim.quarto.pub/interactive-dashboard)

Back to top